﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;

namespace RStein.PosterousReader.Common.ViewModelsHelpers
{
  public class TransientStateHelper : ITransientStateHelper
  {
    private enum PropertyType
    {
      Standard= 0,
      LastInit
    }

    private static Dictionary<Type, MethodInfo> _changeMethodCache = new Dictionary<Type, MethodInfo>();
    public static readonly IEnumerable<String> IGNORE_METHOD_SUFFIX_LIST = new[] { "Command", "Action", "Helper", "Service", "SynchContext"};
    public static readonly IEnumerable<String> LAST_SET_VALUE_METHOD_PREFIX = new[] { "Selected" };
    public static readonly string RAISE_PROPERTY_CHANGED_METHOD_NAME = "RaisePropertyChangedEvent";
    
    private static Dictionary<Type, List<IGrouping<PropertyType, PropertyInfo>>> _propertiesCache = new Dictionary<Type, List<IGrouping<PropertyType, PropertyInfo>>>();
    
    public virtual Dictionary<string, object> GetTransientData(Object obj)
    {
      checkObjectIsNotNull(obj);
      
      if (!IsTransientStateEnabledForObject(obj))
      {
        return null;
      }

      List<IGrouping<PropertyType, PropertyInfo>> properties = getProperties(obj);
      return selectPropertiesData(obj, properties);
    }       

    public virtual void RestoreTransientData(Object obj, Dictionary<string, object> savedState)
    {
      if (savedState == null)
      {
        throw new ArgumentNullException("state");
      }

      if (!IsTransientStateEnabledForObject(obj))
      {
        return;
      }

      PropertyNotificationBase notificationBase = obj as PropertyNotificationBase;
      try
      {
        if (notificationBase != null)
        {
          notificationBase.SuppressPropertyChangedNotification = true;
        }
        List<IGrouping<PropertyType, PropertyInfo>> properties = getProperties(obj);
        restoreProperties(obj, properties, savedState);
        
      }
      finally
      {
        if (notificationBase != null)
        {
          notificationBase.SuppressPropertyChangedNotification = false;
        }
      }
    }

    public virtual bool IsTransientStateEnabledForObject(object obj)
    {
      if (obj == null)
      {
        throw new ArgumentNullException();
      }

      return !obj.GetType()
                .GetCustomAttributes(typeof(NonTransientStateAttribute), false)
                .Any();
    }
    
    private void checkObjectIsNotNull(Object obj)
    {
      if (obj == null)
      {
        throw new ArgumentNullException("obj");
      }
    }

    private void restoreProperties(object obj, List<IGrouping<PropertyType, PropertyInfo>> properties, Dictionary<string, object> state)
    {
                             
      properties.ForEach(propertyGroup => propertyGroup.ToList()
                                                        .ForEach(property => property.SetValue(obj, state[property.Name], null)));

      raiseAllPropertiesChanged(obj, properties);

      
      
      
    }

    private MethodInfo getRaisePropertyChangeMethod(object obj)
    {

          MethodInfo retMethodInfo = null;                      
          var objType = obj.GetType();

          if (_changeMethodCache.TryGetValue(objType, out retMethodInfo))
          {
            return retMethodInfo;
          }

          var firstMethodArgumentType = typeof(string);
          var methodChanged = (from method in obj.GetType().GetMethods()
                               let parameters =  method.GetParameters()
                               where method.Name == RAISE_PROPERTY_CHANGED_METHOD_NAME && 
                                                    parameters.Count() == 1 && 
                                                    parameters[0].ParameterType == firstMethodArgumentType
                               select method).FirstOrDefault();

          _changeMethodCache[objType] = methodChanged;
          return methodChanged;
    }

    private void raiseAllPropertiesChanged(Object obj, List<IGrouping<PropertyType, PropertyInfo>> properties)
    {
      PropertyNotificationBase notificationBase = obj as PropertyNotificationBase;

      if (notificationBase == null)
      {
        tryRaiseAllPropertiesChangedWithReflection(obj, properties);
      }
      else
      {

        var propertyNames = from propertyGroup in properties
                            from property in propertyGroup
                            select property.Name;

        notificationBase.RaisePropertiesChanged(propertyNames.ToArray());
      }
    }

    private void tryRaiseAllPropertiesChangedWithReflection(Object obj, List<IGrouping<PropertyType, PropertyInfo>> properties)
    {
      MethodInfo changeMethod = getRaisePropertyChangeMethod(obj);
      if (changeMethod == null)
      {
        return;
      }
       
      properties.ForEach(propertyGroup => propertyGroup.ToList()
                                                        .ForEach(property => changeMethod.Invoke(obj, new Object[] { property.Name })));
    }

    private List<IGrouping<PropertyType, PropertyInfo>> getProperties(object obj)
    {
      var objType = obj.GetType();
      if (_propertiesCache.ContainsKey(objType))
      {
        return _propertiesCache[objType];
      }

      var properties = objType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy)
                              .Where(property => property.GetIndexParameters().Count() == 0 && 
                                                 property.CanWrite && 
                                                 property.GetSetMethod() != null &&
                                                 !IGNORE_METHOD_SUFFIX_LIST.Any(suffix => property.Name.EndsWith(suffix)) &&
                                                 !property.GetCustomAttributes(typeof(NonTransientStateAttribute), false).Any())
                                                 .GroupBy(property => LAST_SET_VALUE_METHOD_PREFIX.Any(prefix => property.Name.Contains(prefix)) ? PropertyType.LastInit
                                                                                                                                                 : PropertyType.Standard)
                                                                                                                                              
                                                 .OrderBy(group => group.Key)
                            .ToList();
      
      addPropertiesToCache(objType, properties);
      return properties;
    }

    private void addPropertiesToCache(Type objType, List<IGrouping<PropertyType, PropertyInfo>> properties)
    {
      _propertiesCache.Add(objType, properties);
    }

    private Dictionary<string, object> selectPropertiesData(Object obj, List<IGrouping<PropertyType, PropertyInfo>> properties)
    {
      
      var retValues = (from propGroup in properties
                       from property in propGroup
                        select new
                        {
                          Key = property.Name,
                          Value = property.GetValue(obj, null)
                        })
                        .ToDictionary(propValuePair => propValuePair.Key,
                                      propValuePair => propValuePair.Value);

      return retValues;
                                 
                        
    }


    
  }
}
